const {Disposable, CompositeDisposable} = require('event-kit')
const listen = require('./delegated-listener')

// Handles low-level events related to the `window`.
module.exports =
class WindowEventHandler {
  constructor ({atomEnvironment, applicationDelegate}) {
    this.handleDocumentKeyEvent = this.handleDocumentKeyEvent.bind(this)
    this.handleFocusNext = this.handleFocusNext.bind(this)
    this.handleFocusPrevious = this.handleFocusPrevious.bind(this)
    this.handleWindowBlur = this.handleWindowBlur.bind(this)
    this.handleWindowResize = this.handleWindowResize.bind(this)
    this.handleEnterFullScreen = this.handleEnterFullScreen.bind(this)
    this.handleLeaveFullScreen = this.handleLeaveFullScreen.bind(this)
    this.handleWindowBeforeunload = this.handleWindowBeforeunload.bind(this)
    this.handleWindowToggleFullScreen = this.handleWindowToggleFullScreen.bind(this)
    this.handleWindowClose = this.handleWindowClose.bind(this)
    this.handleWindowReload = this.handleWindowReload.bind(this)
    this.handleWindowToggleDevTools = this.handleWindowToggleDevTools.bind(this)
    this.handleWindowToggleMenuBar = this.handleWindowToggleMenuBar.bind(this)
    this.handleLinkClick = this.handleLinkClick.bind(this)
    this.handleDocumentContextmenu = this.handleDocumentContextmenu.bind(this)
    this.atomEnvironment = atomEnvironment
    this.applicationDelegate = applicationDelegate
    this.reloadRequested = false
    this.subscriptions = new CompositeDisposable()

    this.handleNativeKeybindings()
  }

  initialize (window, document) {
    this.window = window
    this.document = document
    this.subscriptions.add(this.atomEnvironment.commands.add(this.window, {
      'window:toggle-full-screen': this.handleWindowToggleFullScreen,
      'window:close': this.handleWindowClose,
      'window:reload': this.handleWindowReload,
      'window:toggle-dev-tools': this.handleWindowToggleDevTools
    }))

    if (['win32', 'linux'].includes(process.platform)) {
      this.subscriptions.add(this.atomEnvironment.commands.add(this.window,
        {'window:toggle-menu-bar': this.handleWindowToggleMenuBar})
      )
    }

    this.subscriptions.add(this.atomEnvironment.commands.add(this.document, {
      'core:focus-next': this.handleFocusNext,
      'core:focus-previous': this.handleFocusPrevious
    }))

    this.addEventListener(this.window, 'beforeunload', this.handleWindowBeforeunload)
    this.addEventListener(this.window, 'focus', this.handleWindowFocus)
    this.addEventListener(this.window, 'blur', this.handleWindowBlur)
    this.addEventListener(this.window, 'resize', this.handleWindowResize)

    this.addEventListener(this.document, 'keyup', this.handleDocumentKeyEvent)
    this.addEventListener(this.document, 'keydown', this.handleDocumentKeyEvent)
    this.addEventListener(this.document, 'drop', this.handleDocumentDrop)
    this.addEventListener(this.document, 'dragover', this.handleDocumentDragover)
    this.addEventListener(this.document, 'contextmenu', this.handleDocumentContextmenu)
    this.subscriptions.add(listen(this.document, 'click', 'a', this.handleLinkClick))
    this.subscriptions.add(listen(this.document, 'submit', 'form', this.handleFormSubmit))

    this.subscriptions.add(this.applicationDelegate.onDidEnterFullScreen(this.handleEnterFullScreen))
    this.subscriptions.add(this.applicationDelegate.onDidLeaveFullScreen(this.handleLeaveFullScreen))
  }

  // Wire commands that should be handled by Chromium for elements with the
  // `.native-key-bindings` class.
  handleNativeKeybindings () {
    const bindCommandToAction = (command, action) => {
      this.subscriptions.add(
        this.atomEnvironment.commands.add(
          '.native-key-bindings',
          command,
          event => this.applicationDelegate.getCurrentWindow().webContents[action](),
          false
        )
      )
    }

    bindCommandToAction('core:copy', 'copy')
    bindCommandToAction('core:paste', 'paste')
    bindCommandToAction('core:undo', 'undo')
    bindCommandToAction('core:redo', 'redo')
    bindCommandToAction('core:select-all', 'selectAll')
    bindCommandToAction('core:cut', 'cut')
  }

  unsubscribe () {
    this.subscriptions.dispose()
  }

  on (target, eventName, handler) {
    target.on(eventName, handler)
    this.subscriptions.add(new Disposable(function () {
      target.removeListener(eventName, handler)
    }))
  }

  addEventListener (target, eventName, handler) {
    target.addEventListener(eventName, handler)
    this.subscriptions.add(new Disposable(function () {
      target.removeEventListener(eventName, handler)
    }))
  }

  handleDocumentKeyEvent (event) {
    this.atomEnvironment.keymaps.handleKeyboardEvent(event)
    event.stopImmediatePropagation()
  }

  handleDrop (event) {
    event.preventDefault()
    event.stopPropagation()
  }

  handleDragover (event) {
    event.preventDefault()
    event.stopPropagation()
    event.dataTransfer.dropEffect = 'none'
  }

  eachTabIndexedElement (callback) {
    for (let element of this.document.querySelectorAll('[tabindex]')) {
      if (element.disabled) { continue }
      if (!(element.tabIndex >= 0)) { continue }
      callback(element, element.tabIndex)
    }
  }

  handleFocusNext () {
    const focusedTabIndex = this.document.activeElement.tabIndex != null ? this.document.activeElement.tabIndex : -Infinity

    let nextElement = null
    let nextTabIndex = Infinity
    let lowestElement = null
    let lowestTabIndex = Infinity
    this.eachTabIndexedElement(function (element, tabIndex) {
      if (tabIndex < lowestTabIndex) {
        lowestTabIndex = tabIndex
        lowestElement = element
      }

      if (focusedTabIndex < tabIndex && tabIndex < nextTabIndex) {
        nextTabIndex = tabIndex
        nextElement = element
      }
    })

    if (nextElement != null) {
      nextElement.focus()
    } else if (lowestElement != null) {
      lowestElement.focus()
    }
  }

  handleFocusPrevious () {
    const focusedTabIndex = this.document.activeElement.tabIndex != null ? this.document.activeElement.tabIndex : Infinity

    let previousElement = null
    let previousTabIndex = -Infinity
    let highestElement = null
    let highestTabIndex = -Infinity
    this.eachTabIndexedElement(function (element, tabIndex) {
      if (tabIndex > highestTabIndex) {
        highestTabIndex = tabIndex
        highestElement = element
      }

      if (focusedTabIndex > tabIndex && tabIndex > previousTabIndex) {
        previousTabIndex = tabIndex
        previousElement = element
      }
    })

    if (previousElement != null) {
      previousElement.focus()
    } else if (highestElement != null) {
      highestElement.focus()
    }
  }

  handleWindowFocus () {
    this.document.body.classList.remove('is-blurred')
  }

  handleWindowBlur () {
    this.document.body.classList.add('is-blurred')
    this.atomEnvironment.storeWindowDimensions()
  }

  handleWindowResize () {
    this.atomEnvironment.storeWindowDimensions()
  }

  handleEnterFullScreen () {
    this.document.body.classList.add('fullscreen')
  }

  handleLeaveFullScreen () {
    this.document.body.classList.remove('fullscreen')
  }

  handleWindowBeforeunload (event) {
    if (!this.reloadRequested && !this.atomEnvironment.inSpecMode() && this.atomEnvironment.getCurrentWindow().isWebViewFocused()) {
      this.atomEnvironment.hide()
    }
    this.reloadRequested = false
    this.atomEnvironment.storeWindowDimensions()
    this.atomEnvironment.unloadEditorWindow()
    this.atomEnvironment.destroy()
  }

  handleWindowToggleFullScreen () {
    this.atomEnvironment.toggleFullScreen()
  }

  handleWindowClose () {
    this.atomEnvironment.close()
  }

  handleWindowReload () {
    this.reloadRequested = true
    this.atomEnvironment.reload()
  }

  handleWindowToggleDevTools () {
    this.atomEnvironment.toggleDevTools()
  }

  handleWindowToggleMenuBar () {
    this.atomEnvironment.config.set('core.autoHideMenuBar', !this.atomEnvironment.config.get('core.autoHideMenuBar'))

    if (this.atomEnvironment.config.get('core.autoHideMenuBar')) {
      const detail = 'To toggle, press the Alt key or execute the window:toggle-menu-bar command'
      this.atomEnvironment.notifications.addInfo('Menu bar hidden', {detail})
    }
  }

  handleLinkClick (event) {
    event.preventDefault()
    const uri = event.currentTarget && event.currentTarget.getAttribute('href')
    if (uri && uri[0] !== '#') {
      if (/^https?:\/\//.test(uri)) {
        this.applicationDelegate.openExternal(uri)
      } else if (uri.startsWith('atom://')) {
        this.atomEnvironment.uriHandlerRegistry.handleURI(uri)
      }
    }
  }

  handleFormSubmit (event) {
    // Prevent form submits from changing the current window's URL
    event.preventDefault()
  }

  handleDocumentContextmenu (event) {
    event.preventDefault()
    this.atomEnvironment.contextMenu.showForEvent(event)
  }
}
